﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using Steve.UI;
using Lucene.Net.Index;
using Lucene.Net.Documents;
using Lucene.Net.Analysis.Snowball;
using Lucene.Net.QueryParsers;
using System.IO;
using Lucene.Net.Search;
using Lucene.Net.Analysis;

using LuceneDirectory = Lucene.Net.Store.Directory;
using LuceneVersion = Lucene.Net.Util.Version;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace Steve.Lucene
{
    [Export(typeof(ISearchIndexService))]
    [Export(typeof(ISearchQueryService))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public sealed class LuceneSearchService : ISearchIndexService, ISearchQueryService
    {
        const string TitleFieldName = "Title";
        const string TeaserFieldName = "Teaser";
        const string DataFieldName = "Data";
        const string RevisionNumberFieldName = "Revision";
        const string IdFieldName = "Id";
        const string ContentTypeFieldName = "ContentType";
        const string UrlFieldName = "Url";

        [Import]
        public ILuceneDirectoryBuilder DirectoryBuilder { get; set; }
        [Import]
        public ILoggingService Log { get; set; }
        [Import]
        public IConfiguration Config { get; set; }
        [Import]
        public ContentRepository ContentRepository { get; set; }
        [Import]
        public ContentTypeRepository ContentTypeRepository { get; set; }
        [ImportMany(typeof(ContentDataHandlerBase))]
        public IEnumerable<Lazy<ContentDataHandlerBase, IDictionary<string, object>>> DataHandlers { get; set; }

        private void AddContentToIndex(IndexWriter writer, Content c)
        {
            var ct = ContentTypeRepository.GetContentType(c.ContentTypeName);
            var doc = new Document();

            doc.Add(new Field(IdFieldName, c.Id.ToString("D", CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NO));
            doc.Add(new Field(RevisionNumberFieldName, c.RevisionNumber.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NO));
            doc.Add(new Field(TitleFieldName, c.Title, Field.Store.YES, Field.Index.ANALYZED));
            doc.Add(new Field(TeaserFieldName, c.Teaser, Field.Store.YES, Field.Index.ANALYZED));
            doc.Add(new Field(UrlFieldName, c.ToUri().ToString(), Field.Store.YES, Field.Index.NO));

            var text = DataHandlers.ExtractText(ct, c);

            if (text != null) doc.Add(new Field(DataFieldName, text, Field.Store.YES, Field.Index.ANALYZED));

            writer.AddDocument(doc);
        }

        //TODO: globalization...
        private Analyzer GetAnalyzer()
        {
            return new SnowballAnalyzer("English", Config.SearchStopWords);
        }

        public SearchResult Search(SearchType searchType, string rawQuery)
        {
            var parser = new MultiFieldQueryParser(LuceneVersion.LUCENE_29, new string[] { TitleFieldName, DataFieldName, TeaserFieldName }, GetAnalyzer());
            var query = parser.Parse(rawQuery);

            try
            {
                var searcher = new IndexSearcher(DirectoryBuilder.GetDirectory(searchType), true);
                //TODO: move count somewhere else...
                var topDocs = searcher.Search(query, Config.MaxSearchResults);
                var result = new SearchResult();

                result.TotalCount = topDocs.totalHits;
                result.ItemCount = topDocs.scoreDocs.Length;
                result.MaxScore = topDocs.GetMaxScore();

                var scoreNorm = 1.0f / result.MaxScore;

                var topDocTransform = from sd in topDocs.scoreDocs
                                      let doc = searcher.GetIndexReader().Document(sd.doc)
                                      select new SearchResultItem()
                                      {
                                          ContentId = Guid.Parse(doc.Get(IdFieldName)),
                                          RevisionNumber = int.Parse(doc.Get(RevisionNumberFieldName)),
                                          RawScore = sd.score,
                                          NormalizedScore = sd.score * scoreNorm,
                                          Teaser = doc.Get(TeaserFieldName),
                                          Title = doc.Get(TitleFieldName),
                                          ContentTypeName = doc.Get(ContentTypeFieldName),
                                          Url = new Uri(doc.Get(UrlFieldName))
                                      };

                result.Items = topDocTransform.ToList();
                searcher.Close();

                return result;
            }
            catch (FileNotFoundException)
            {
                Log.Warn("LuceneSearch", "Unable to open search index.");
                return null;
            }
        }

        private void IndexCore(IEnumerable<Tuple<Guid, int>> contentIds, bool isFull)
        {
            var analyzer = GetAnalyzer();
            var approvedWriter = new IndexWriter(DirectoryBuilder.GetDirectory(SearchType.LatestApproved), analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED);
            var latestWriter = new IndexWriter(DirectoryBuilder.GetDirectory(SearchType.Latest), analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED);
            var revisionWriter = new IndexWriter(DirectoryBuilder.GetDirectory(SearchType.RevisionHistory), analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED);
            
            if (isFull)
            {
                Log.Info("LuceneIndex", "Cleaning index.");
                approvedWriter.DeleteAll();
                latestWriter.DeleteAll();
                revisionWriter.DeleteAll();
            }

            Log.Info("LuceneIndex", "Beginning index.");
            
            var group = (from t in contentIds
                         group t by t.Item1 into contentId
                         select contentId).ToList();

            int count = 0;
            foreach (var u in group)
            {
                if (!isFull)
                {
                    latestWriter.DeleteDocuments(new Term(IdFieldName, u.Key.ToString()));
                    approvedWriter.DeleteDocuments(new Term(IdFieldName, u.Key.ToString()));
                }

                var latest = ContentRepository.GetContent(u.Key, null, false);
                Content approved;

                //save the lookup
                if (latest.RevisionNumber == latest.LatestApprovedRevisionNumber)
                    approved = latest;
                else
                    approved = ContentRepository.GetContent(u.Key, null, true);

                AddContentToIndex(latestWriter, latest);

                if(approved != null)
                    AddContentToIndex(approvedWriter, approved);

                foreach (var r in u)
                {
                    Content revision;

                    //save the lookup
                    if (latest.RevisionNumber == r.Item2)
                        revision = latest;
                    else if (approved != null && approved.RevisionNumber == r.Item2)
                        revision = approved;
                    else
                        revision = ContentRepository.GetContent(r.Item1, r.Item2, false);

                    AddContentToIndex(revisionWriter, revision);
                }

                count++;
            }

            Log.Info("LuceneIndex", string.Format(CultureInfo.InvariantCulture, "Finished adding {0} items to index.", count));

            FinishWriter(approvedWriter);
            FinishWriter(latestWriter);
            FinishWriter(revisionWriter);

            Log.Info("LuceneIndex", "Finished optimizing and committing index.");
        }

        private void FinishWriter(IndexWriter writer)
        {
            writer.Optimize();
            writer.Commit();
            writer.Close();
        }

        public void Index(IEnumerable<Tuple<Guid, int>> contentIds)
        {
            if (contentIds == null) throw new ArgumentNullException("contentIds");

            IndexCore(contentIds, false);
        }

        public void RebuildIndex()
        {
            //restart the index from scratch...
            var updated = ContentRepository.GetUpdatedContentIds(null);

            IndexCore(updated, true);
        }
    }
}
